package com.talkweb.osmharmony.views;

import com.talkweb.osmharmony.api.IGeoPoint;
import com.talkweb.osmharmony.api.IMapController;
import com.talkweb.osmharmony.api.IMapView;
import com.talkweb.osmharmony.config.Configuration;
import com.talkweb.osmharmony.events.MapListener;
import com.talkweb.osmharmony.events.ScrollEvent;
import com.talkweb.osmharmony.events.ZoomEvent;
import com.talkweb.osmharmony.tileprovider.MapTileProviderBase;
import com.talkweb.osmharmony.tileprovider.MapTileProviderBasic;
import com.talkweb.osmharmony.tileprovider.tilesource.ITileSource;
import com.talkweb.osmharmony.tileprovider.tilesource.TileSourceFactory;
import com.talkweb.osmharmony.tileprovider.util.SimpleInvalidationHandler;
import com.talkweb.osmharmony.util.*;
import com.talkweb.osmharmony.views.overlay.DefaultOverlayManager;
import com.talkweb.osmharmony.views.overlay.Overlay;
import com.talkweb.osmharmony.views.overlay.OverlayManager;
import com.talkweb.osmharmony.views.overlay.TilesOverlay;
import ohos.agp.components.*;
import ohos.agp.render.Canvas;
import ohos.agp.utils.Point;
import ohos.agp.utils.Rect;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.multimodalinput.event.KeyEvent;
import ohos.multimodalinput.event.MmiPoint;
import ohos.multimodalinput.event.TouchEvent;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * This is the primary view for osmdroid. <br><br>
 * As of version 6.0.0, please respect the android view lifecycle by calling
 * {@link MapView#onPause()} and {@link MapView#onResume()} respectively
 *
 * @author plusminus on 17:45:56 - 25.09.2008
 * @author and many other contributors
 * @since the begining
 */
public class MapView extends ComponentContainer implements IMapView,
        Component.EstimateSizeListener,
        ComponentContainer.ArrangeListener,
        Component.BindStateChangedListener,
        Component.KeyEventListener,
        Component.DrawTask {

    /**
     * Current zoom level for map tiles.
     */
    private double mZoomLevel = 0;

    private OverlayManager mOverlayManager;

    protected Projection mProjection;

    private TilesOverlay mMapOverlay;

    /**
     * Handles map scrolling
     */
    private final ScrollHelper mScroller;
    protected boolean mIsFlinging;
    /**
     * Set to true when the `Projection` actually adjusted the scroll values
     * Consequence: on this side effect, we must stop the flinging
     */
    private boolean mImpossibleFlinging;

    protected final AtomicBoolean mIsAnimating = new AtomicBoolean(false);

    protected Double mMinimumZoomLevel;
    protected Double mMaximumZoomLevel;

    private final MapController mController;

    private final CustomZoomButtonsController mZoomController;

    /**
     * Initial pinch gesture pixel (typically, the middle of both fingers)
     */
    private final Point mMultiTouchScaleInitPoint = new Point();
    /**
     * Initial pinch gesture geo point, computed from {@link MapView#mMultiTouchScaleInitPoint}
     * and the current Projection
     */
    private final GeoPoint mMultiTouchScaleGeoPoint = new GeoPoint(0., 0);
    /**
     * Current pinch gesture pixel (again, the middle of both fingers)
     * We must ensure that this pixel is the projection of {@link MapView#mMultiTouchScaleGeoPoint}
     */
    private Point mMultiTouchScaleCurrentPoint;

    private boolean mMultiTouchControlsEnable = false;


    // For rotation
    private float mapOrientation = 0;
    private final Rect mInvalidateRect = new Rect();

    private boolean mScrollableAreaLimitLatitude;
    private double mScrollableAreaLimitNorth;
    private double mScrollableAreaLimitSouth;
    private boolean mScrollableAreaLimitLongitude;
    private double mScrollableAreaLimitWest;
    private double mScrollableAreaLimitEast;
    private int mScrollableAreaLimitExtraPixelWidth;
    private int mScrollableAreaLimitExtraPixelHeight;

    private MapTileProviderBase mTileProvider;
    private EventHandler mTileRequestCompleteHandler;
    private boolean mTilesScaledToDpi = false;
    private float mTilesScaleFactor = 1f;

    final Point mRotateScalePoint = new Point();

    /* a point that will be reused to lay out added views */
    private final Point mLayoutPoint = new Point();

    // Keep a set of listeners for when the maps have a layout
    private final LinkedList<OnFirstLayoutListener> mOnFirstLayoutListeners = new LinkedList<OnFirstLayoutListener>();

    /* becomes true once onLayout has been called for the first time i.e. map is ready to go. */
    private boolean mLayoutOccurred = false;

    private boolean horizontalMapRepetitionEnabled = true;
    private boolean verticalMapRepetitionEnabled = true;

    private GeoPoint mCenter;
    private long mMapScrollX;
    private long mMapScrollY;
    protected List<MapListener> mListners = new ArrayList<>();

    private double mStartAnimationZoom;

    private boolean mZoomRounding;

    /**
     * @since 6.0.3
     */
    private final MapViewRepository mRepository = new MapViewRepository(this);

    public interface OnFirstLayoutListener {
        /**
         * this generally means that the map is ready to go
         *
         * @param v
         * @param left
         * @param top
         * @param right
         * @param bottom
         */
        void onFirstLayout(Component v, int left, int top, int right, int bottom);
    }

    private static TileSystem mTileSystem = new TileSystemWebMercator();

    /**
     * @since 6.1.0
     */
    private final Rect mRescaleScreenRect = new Rect(); // optimization

    /**
     * @since 6.1.0
     * cf. https://github.com/osmdroid/osmdroid/issues/1247
     */
    private boolean mDestroyModeOnDetach = true;

    /**
     * @since 6.1.1
     * The map center used to be projected into the screen center.
     * Now we have a possible offset from the screen center; default offset is [0, 0].
     */
    private int mMapCenterOffsetX;
    private int mMapCenterOffsetY;

    // ===========================================================
    // Constructors
    // ===========================================================

    public MapView(final Context context) {
        this(context, null);
    }

    /**
     * Constructor used by XML layout resource (uses default tile source).
     */
    public MapView(final Context context, final AttrSet attrs) {
        this(context, attrs, null);
    }

    public MapView(Context context, AttrSet attrs, String styleName) {
        super(context, attrs, styleName);
        Configuration.getInstance().getOsmdroidTileCache(context);

        this.mController = new MapController(this);
        this.mScroller = new ScrollHelper();

        mTileRequestCompleteHandler = new SimpleInvalidationHandler(this);
        final ITileSource tileSource = getTileSourceFromAttributes(attrs);
        mTileProvider = new MapTileProviderBasic(context.getApplicationContext(), tileSource);
        mTileProvider.getTileRequestCompleteHandlers().add(mTileRequestCompleteHandler);
        updateTileSizeForDensity(mTileProvider.getTileSource());

        this.mMapOverlay = new TilesOverlay(mTileProvider, context, horizontalMapRepetitionEnabled, verticalMapRepetitionEnabled);
        mOverlayManager = new DefaultOverlayManager(mMapOverlay);

        mZoomController = new CustomZoomButtonsController(this);
        mZoomController.setOnZoomListener(new MapViewZoomListener());
        checkZoomButtons();

        mZoomController.setVisibility(CustomZoomButtonsController.Visibility.SHOW_AND_FADEOUT);

        setEstimateSizeListener(this);
        setArrangeListener(this);
        setBindStateChangedListener(this);
        setKeyEventListener(this);

        MapViewGestureDetector mapViewGestureDetector = new MapViewGestureDetector();
        setTouchEventListener(mapViewGestureDetector);
        setLongClickedListener(mapViewGestureDetector);
        setClickedListener(mapViewGestureDetector);
        setDoubleClickedListener(mapViewGestureDetector);
        setDraggedListener(DRAG_HORIZONTAL_VERTICAL, mapViewGestureDetector);
        setScaledListener(new MapViewScaledListener());
        addDrawTask(this, DrawTask.BETWEEN_BACKGROUND_AND_CONTENT);
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    @Override
    public IMapController getController() {
        return this.mController;
    }

    /**
     * You can add/remove/reorder your Overlays using the List of {@link Overlay}. The first (index
     * 0) Overlay gets drawn first, the one with the highest as the last one.
     */
    public List<Overlay> getOverlays() {
        return this.getOverlayManager().overlays();
    }

    public OverlayManager getOverlayManager() {
        return mOverlayManager;
    }

    public void setOverlayManager(final OverlayManager overlayManager) {
        mOverlayManager = overlayManager;
    }

    public MapTileProviderBase getTileProvider() {
        return mTileProvider;
    }

    public ScrollHelper getScroller() {
        return mScroller;
    }

    public EventHandler getTileRequestCompleteHandler() {
        return mTileRequestCompleteHandler;
    }

    @Override
    public double getLatitudeSpanDouble() {
        return this.getBoundingBox().getLatitudeSpan();
    }

    @Override
    public double getLongitudeSpanDouble() {
        return this.getBoundingBox().getLongitudeSpan();
    }

    public BoundingBox getBoundingBox() {
        return getProjection().getBoundingBox();
    }


    /**
     * Gets the current bounds of the screen in <I>screen coordinates</I>.
     */
    public Rect getScreenRect(final Rect reuse) {
        final Rect out = getIntrinsicScreenRect(reuse);
        if (this.getMapOrientation() != 0 && this.getMapOrientation() != 180) {
            GeometryMath.getBoundingBoxForRotatatedRectangle(out, out.getCenterX(), out.getCenterY(),
                    this.getMapOrientation(), out);
        }
        return out;
    }

    public Rect getIntrinsicScreenRect(final Rect reuse) {
        final Rect out = reuse == null ? new Rect() : reuse;
        out.set(0, 0, getWidth(), getHeight());
        return out;
    }

    /**
     * Get a projection for converting between screen-pixel coordinates and latitude/longitude
     * coordinates. You should not hold on to this object for more than one draw, since the
     * projection of the map could change.
     *
     * @return The Projection of the map in its current state. You should not hold on to this object
     * for more than one draw, since the projection of the map could change.
     */
    @Override
    public Projection getProjection() {
        if (mProjection == null) {
            Projection localCopy = new Projection(this);
            mProjection = localCopy;
            localCopy.adjustOffsets(mMultiTouchScaleGeoPoint, mMultiTouchScaleCurrentPoint);
            if (mScrollableAreaLimitLatitude) {
                localCopy.adjustOffsets(
                        mScrollableAreaLimitNorth, mScrollableAreaLimitSouth, true,
                        mScrollableAreaLimitExtraPixelHeight);
            }
            if (mScrollableAreaLimitLongitude) {
                localCopy.adjustOffsets(
                        mScrollableAreaLimitWest, mScrollableAreaLimitEast, false,
                        mScrollableAreaLimitExtraPixelWidth);
            }
            mImpossibleFlinging = localCopy.setMapScroll(this);
        }
        return mProjection;
    }

    private void resetProjection() {
        mProjection = null;
    }

    public boolean isTilesScaledToDpi() {
        return mTilesScaledToDpi;
    }

    /**
     * if true, tiles are scaled to the current DPI of the display. This effectively
     * makes it easier to read labels, how it may appear pixelated depending on the map
     * source.<br>
     * if false, tiles are rendered in their real size
     *
     * @param tilesScaledToDpi
     */
    public void setTilesScaledToDpi(boolean tilesScaledToDpi) {
        mTilesScaledToDpi = tilesScaledToDpi;
        updateTileSizeForDensity(getTileProvider().getTileSource());
    }

    public float getTilesScaleFactor() {
        return mTilesScaleFactor;
    }

    /**
     * Setting an additional scale factor both for ScaledToDpi and standard size
     * > 1.0 enlarges map display, < 1.0 shrinks map display
     *
     * @since 6.1.0
     */
    public void setTilesScaleFactor(float pTilesScaleFactor) {
        mTilesScaleFactor = pTilesScaleFactor;
        updateTileSizeForDensity(getTileProvider().getTileSource());
    }

    public void resetTilesScaleFactor() {
        mTilesScaleFactor = 1f;
        updateTileSizeForDensity(getTileProvider().getTileSource());
    }

    private void updateTileSizeForDensity(final ITileSource aTileSource) {
        int tile_size = aTileSource.getTileSizePixels();
        float scaleDensity = DisplayUtils.getDisplayAttributes(getContext()).densityPixels;
        float density = scaleDensity * 256 / tile_size;
        int size = (int) (tile_size * (isTilesScaledToDpi() ? density * mTilesScaleFactor : mTilesScaleFactor));
        if (Configuration.getInstance().isDebugMapView())
            Log.d(IMapView.LOGTAG, "Scaling tiles to " + size);
        TileSystem.setTileSize(size);
    }

    public void setTileSource(final ITileSource aTileSource) {
        mTileProvider.setTileSource(aTileSource);
        updateTileSizeForDensity(aTileSource);
        this.checkZoomButtons();
        this.setZoomLevel(mZoomLevel); // revalidate zoom level
        postInvalidate();
    }

    /**
     * @param aZoomLevel the zoom level bound by the tile source
     *                   Used to be an int - is a double since 6.0
     */
    double setZoomLevel(final double aZoomLevel) {
        final double newZoomLevel = Math.max(getMinZoomLevel(), Math.min(getMaxZoomLevel(), aZoomLevel));
        final double curZoomLevel = this.mZoomLevel;

        if (newZoomLevel != curZoomLevel) {
            if (mScroller != null)    //fix for edit mode in the IDE
                mScroller.abortAnimation();
            mIsFlinging = false;
        }

        // Get our current center point
        final IGeoPoint centerGeoPoint = getProjection().getCurrentCenter();

        this.mZoomLevel = newZoomLevel;

        setExpectedCenter(centerGeoPoint);
        this.checkZoomButtons();

        if (isLayoutOccurred()) {
            getController().setCenter(centerGeoPoint);

            // snap for all snappables
            final Point snapPoint = new Point();
            final Projection pj = getProjection();
            if (this.getOverlayManager().onSnapToItem(mMultiTouchScaleInitPoint.getPointXToInt(),
                    mMultiTouchScaleInitPoint.getPointYToInt(), snapPoint, this)) {
                IGeoPoint geoPoint = pj.fromPixels(snapPoint.getPointXToInt(), snapPoint.getPointYToInt(), null, false);
                getController().animateTo(geoPoint);
            }

            mTileProvider.rescaleCache(pj, newZoomLevel, curZoomLevel, getScreenRect(mRescaleScreenRect));
        }

        // do callback on listener
        if (newZoomLevel != curZoomLevel) {
            ZoomEvent event = null;
            for (MapListener mapListener : mListners)
                mapListener.onZoom(event != null ? event : (event = new ZoomEvent(this, newZoomLevel)));
        }

        postLayout(); // Allows any views fixed to a Location in the MapView to adjust
        invalidate();
        return this.mZoomLevel;
    }

    /**
     * Zoom the map to enclose the specified bounding box, as closely as possible. Must be called
     * after display layout is complete, or screen dimensions are not known, and will always zoom to
     * center of zoom level 0.<br>
     * Suggestion: Check getIntrinsicScreenRect(null).getHeight() &gt; 0
     */
    public void zoomToBoundingBox(final BoundingBox boundingBox, final boolean animated) {
        zoomToBoundingBox(boundingBox, animated, 0);
    }

    /**
     * @param pBoundingBox        Bounding box we want to zoom to; may be a single {@link GeoPoint}
     * @param pAnimated           Animation or immediate action?
     * @param pBorderSizeInPixels Border size around the bounding box
     * @param pMaximumZoom        Maximum zoom we want from bounding box computation
     * @param pAnimationSpeed     Animation duration, in milliseconds
     * @since 6.0.3
     */
    public double zoomToBoundingBox(final BoundingBox pBoundingBox, final boolean pAnimated,
                                    final int pBorderSizeInPixels, final double pMaximumZoom,
                                    final Long pAnimationSpeed) {
        double nextZoom = mTileSystem.getBoundingBoxZoom(pBoundingBox, getWidth() - 2 * pBorderSizeInPixels, getHeight() - 2 * pBorderSizeInPixels);
        if (nextZoom == Double.MIN_VALUE // e.g. single point bounding box
                || nextZoom > pMaximumZoom) { // e.g. tiny bounding box
            nextZoom = pMaximumZoom;
        }
        nextZoom = Math.min(getMaxZoomLevel(), Math.max(nextZoom, getMinZoomLevel()));
        final GeoPoint center = pBoundingBox.getCenterWithDateLine();

        // fine-tuning the latitude, cf. https://github.com/osmdroid/osmdroid/issues/1239
        final Projection projection = new Projection(
                nextZoom, getWidth(), getHeight(),
                center,
                getMapOrientation(),
                isHorizontalMapRepetitionEnabled(), isVerticalMapRepetitionEnabled(),
                getMapCenterOffsetX(), getMapCenterOffsetY());
        final Point point = new Point();
        final double longitude = pBoundingBox.getCenterLongitude();
        projection.toPixels(new GeoPoint(pBoundingBox.getActualNorth(), longitude), point);
        final int north = point.getPointYToInt();
        projection.toPixels(new GeoPoint(pBoundingBox.getActualSouth(), longitude), point);
        final int south = point.getPointYToInt();
        final int offset = ((getHeight() - south) - north) / 2;
        if (offset != 0) {
            projection.adjustOffsets(0, offset);
            projection.fromPixels(getWidth() / 2, getHeight() / 2, center);
        }

        if (pAnimated) {
            getController().animateTo(center, nextZoom, pAnimationSpeed);
        } else { // it's best to set the zoom first, so that the center is accurate
            getController().setZoom(nextZoom);
            getController().setCenter(center);
        }
        return nextZoom;
    }

    /**
     * @since 6.0.0
     */
    public void zoomToBoundingBox(final BoundingBox pBoundingBox, final boolean pAnimated, final int pBorderSizeInPixels) {
        zoomToBoundingBox(pBoundingBox, pAnimated, pBorderSizeInPixels, getMaxZoomLevel(), null);
    }

    /**
     * Get the current ZoomLevel for the map tiles.
     *
     * @return the current ZoomLevel between 0 (equator) and 18/19(closest), depending on the tile
     * source chosen.
     * @since 6.0
     */
    @Override
    public double getZoomLevelDouble() {
        return mZoomLevel;
    }

    /**
     * Get the minimum allowed zoom level for the maps.
     */
    public double getMinZoomLevel() {
        return mMinimumZoomLevel == null ? mMapOverlay.getMinimumZoomLevel() : mMinimumZoomLevel;
    }

    /**
     * Get the maximum allowed zoom level for the maps.
     */
    @Override
    public double getMaxZoomLevel() {
        return mMaximumZoomLevel == null ? mMapOverlay.getMaximumZoomLevel() : mMaximumZoomLevel;
    }

    /**
     * Set the minimum allowed zoom level, or pass null to use the minimum zoom level from the tile
     * provider.
     */
    public void setMinZoomLevel(Double zoomLevel) {
        mMinimumZoomLevel = zoomLevel;
    }

    /**
     * Set the maximum allowed zoom level, or pass null to use the maximum zoom level from the tile
     * provider.
     */
    public void setMaxZoomLevel(Double zoomLevel) {
        mMaximumZoomLevel = zoomLevel;
    }

    public boolean canZoomIn() {
        return mZoomLevel < getMaxZoomLevel();
    }

    public boolean canZoomOut() {
        return mZoomLevel > getMinZoomLevel();
    }

    /**
     * Returns the current center-point position of the map, as a GeoPoint (latitude and longitude).
     * <br><br>
     * Gives you the actual current map center, as the Projection computes it from the middle of
     * the screen. Most of the time it's supposed to be approximately the same value (because
     * of computing rounding side effects), but in some cases (current zoom gesture or scroll
     * limits) the values may differ (typically, when {@link Projection#adjustOffsets} had to fine-tune
     * the map center).
     *
     * @return A GeoPoint of the map's center-point.
     * @see #getExpectedCenter()
     */
    @Override
    public IGeoPoint getMapCenter() {
        return getMapCenter(null);
    }

    /**
     * @since 6.0.3
     */
    public IGeoPoint getMapCenter(GeoPoint pReuse) {
        return getProjection().fromPixels(getWidth() / 2, getHeight() / 2, pReuse, false);
    }

    /**
     * rotates the map to the desired heading
     *
     * @param degrees
     */
    public void setMapOrientation(float degrees) {
        setMapOrientation(degrees, true);
    }

    /**
     * There are some cases when we don't need explicit redraw
     *
     * @since 6.0.0
     */
    public void setMapOrientation(final float degrees, final boolean forceRedraw) {
        mapOrientation = degrees % 360.0f;

        if (forceRedraw) {
            postLayout(); // Allows any views fixed to a Location in the MapView to adjust
            invalidate();
        }
    }

    public float getMapOrientation() {
        return mapOrientation;
    }

    /**
     * @since 6.0.0
     */
    @Deprecated
    public float getMapScale() {
        return 1;
    }

    /**
     * Whether to use the network connection if it's available.
     */
    public boolean useDataConnection() {
        return mMapOverlay.useDataConnection();
    }

    /**
     * Set whether to use the network connection if it's available.
     *
     * @param aMode if true use the network connection if it's available. if false don't use the
     *              network connection even if it's available.
     */
    public void setUseDataConnection(final boolean aMode) {
        mMapOverlay.setUseDataConnection(aMode);
    }

    /**
     * Set the map to limit it's scrollable view to the specified BoundingBox. Note this does not
     * limit zooming so it will be possible for the user to zoom out to an area that is larger than the
     * limited area.
     *
     * @param boundingBox A lat/long bounding box to limit scrolling to, or null to remove any scrolling
     *                    limitations
     */
    public void setScrollableAreaLimitDouble(BoundingBox boundingBox) {
        if (boundingBox == null) {
            resetScrollableAreaLimitLatitude();
            resetScrollableAreaLimitLongitude();
        } else {
            setScrollableAreaLimitLatitude(boundingBox.getActualNorth(), boundingBox.getActualSouth(), 0);
            setScrollableAreaLimitLongitude(boundingBox.getLonWest(), boundingBox.getLonEast(), 0);
        }
    }

    /**
     * @since 6.0.0
     */
    public void resetScrollableAreaLimitLatitude() {
        mScrollableAreaLimitLatitude = false;
    }

    /**
     * @since 6.0.0
     */
    public void resetScrollableAreaLimitLongitude() {
        mScrollableAreaLimitLongitude = false;
    }

    /**
     * sets the scroll limit
     * Example:
     * To block vertical scroll of the view outside north/south poles:
     * mapView.setScrollableAreaLimitLatitude(MapView.getTileSystem().getMaxLatitude(),
     * MapView.getTileSystem().getMinLatitude(),
     * 0);
     * Warning:
     * Don't use latitude values outside the [MapView.getTileSystem().getMinLatitude(),
     * MapView.getTileSystem().getMaxLatitude()] range, this would cause an ANR.
     *
     * @param pNorth            decimal degrees latitude
     * @param pSouth            decimal degrees latitude
     * @param pExtraPixelHeight in pixels, enables scrolling this many pixels past the bounds
     * @since 6.0.0
     */
    public void setScrollableAreaLimitLatitude(final double pNorth, final double pSouth,
                                               final int pExtraPixelHeight) {
        mScrollableAreaLimitLatitude = true;
        mScrollableAreaLimitNorth = pNorth;
        mScrollableAreaLimitSouth = pSouth;
        mScrollableAreaLimitExtraPixelHeight = pExtraPixelHeight;
    }

    /**
     * sets the scroll limit
     *
     * @param pWest            decimal degrees longitude
     * @param pEast            decimal degrees longitude
     * @param pExtraPixelWidth in pixels, enables scrolling this many pixels past the bounds
     * @since 6.0.0
     */
    public void setScrollableAreaLimitLongitude(final double pWest, final double pEast, final int pExtraPixelWidth) {
        mScrollableAreaLimitLongitude = true;
        mScrollableAreaLimitWest = pWest;
        mScrollableAreaLimitEast = pEast;
        mScrollableAreaLimitExtraPixelWidth = pExtraPixelWidth;
    }

    /**
     * @since 6.0.0
     */
    public boolean isScrollableAreaLimitLatitude() {
        return mScrollableAreaLimitLatitude;
    }

    /**
     * @since 6.0.0
     */
    public boolean isScrollableAreaLimitLongitude() {
        return mScrollableAreaLimitLongitude;
    }


    public void invalidateMapCoordinates(Rect dirty) {
        invalidateMapCoordinates(dirty.left, dirty.top, dirty.right, dirty.bottom, false);
    }

    public void invalidateMapCoordinates(int left, int top, int right, int bottom) {
        invalidateMapCoordinates(left, top, right, bottom, false);
    }

    public void postInvalidateMapCoordinates(int left, int top, int right, int bottom) {
        invalidateMapCoordinates(left, top, right, bottom, true);
    }

    private void invalidateMapCoordinates(int left, int top, int right, int bottom, boolean post) {
        mInvalidateRect.set(left, top, right, bottom);

        final int centerX = getWidth() / 2;
        final int centerY = getHeight() / 2;

        if (this.getMapOrientation() != 0)
            GeometryMath.getBoundingBoxForRotatatedRectangle(mInvalidateRect, centerX, centerY,
                    this.getMapOrientation() + 180, mInvalidateRect);

        //TODO 后面实现此步骤
        if (post) {
            postInvalidate();
        } else {
            invalidate();
        }
    }

    @Override
    public boolean onEstimateSize(int widthEstimatedConfig, int heightEstimatedConfig) {
        // 通知子组件进行测量
        measureChildren(widthEstimatedConfig, heightEstimatedConfig);

        int width = Component.EstimateSpec.getSize(widthEstimatedConfig);
        int height = Component.EstimateSpec.getSize(heightEstimatedConfig);

        setEstimatedSize(
                Component.EstimateSpec.getChildSizeWithMode(width, widthEstimatedConfig, EstimateSpec.UNCONSTRAINT),
                Component.EstimateSpec.getChildSizeWithMode(height, heightEstimatedConfig, EstimateSpec.UNCONSTRAINT));
        return true;
    }

    private void measureChildren(int widthEstimatedConfig, int heightEstimatedConfig) {
        for (int idx = 0; idx < getChildCount(); idx++) {
            Component childView = getComponentAt(idx);
            if (childView != null) {
                measureChild(childView, widthEstimatedConfig, heightEstimatedConfig);
            }
        }
    }

    private void measureChild(Component child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
        ComponentContainer.LayoutConfig lc = child.getLayoutConfig();
        int childWidthMeasureSpec = EstimateSpec.getChildSizeWithMode(
                lc.width, parentWidthMeasureSpec, EstimateSpec.UNCONSTRAINT);
        int childHeightMeasureSpec = EstimateSpec.getChildSizeWithMode(
                lc.height, parentHeightMeasureSpec, EstimateSpec.UNCONSTRAINT);
        child.estimateSize(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    @Override
    public boolean onArrange(int left, int top, int width, int height) {
        resetProjection();
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            Component child = getComponentAt(i);
            if (child.getVisibility() != HIDE) {

                final LayoutConfig lp = (LayoutConfig) child.getLayoutConfig();
                final int childHeight = child.getEstimatedHeight();
                final int childWidth = child.getEstimatedWidth();
                getProjection().toPixels(lp.geoPoint, mLayoutPoint);
                // Apply rotation of mLayoutPoint around the center of the map
                if (getMapOrientation() != 0) {
                    final int layoutPointX = mLayoutPoint.getPointXToInt();
                    final int layoutPointY = mLayoutPoint.getPointYToInt();
                    Point p = getProjection().rotateAndScalePoint(layoutPointX, layoutPointY, null);
                    mLayoutPoint.modify(p.getPointX(), p.getPointY());
                }
                final int x = mLayoutPoint.getPointXToInt();
                final int y = mLayoutPoint.getPointYToInt();
                int childLeft = x;
                int childTop = y;
                switch (lp.alignment) {
                    case LayoutConfig.TOP_LEFT:
                        childLeft = getPaddingLeft() + x;
                        childTop = getPaddingTop() + y;
                        break;
                    case LayoutConfig.TOP_CENTER:
                        childLeft = getPaddingLeft() + x - childWidth / 2;
                        childTop = getPaddingTop() + y;
                        break;
                    case LayoutConfig.TOP_RIGHT:
                        childLeft = getPaddingLeft() + x - childWidth;
                        childTop = getPaddingTop() + y;
                        break;
                    case LayoutConfig.CENTER_LEFT:
                        childLeft = getPaddingLeft() + x;
                        childTop = getPaddingTop() + y - childHeight / 2;
                        break;
                    case LayoutConfig.CENTER:
                        childLeft = getPaddingLeft() + x - childWidth / 2;
                        childTop = getPaddingTop() + y - childHeight / 2;
                        break;
                    case LayoutConfig.CENTER_RIGHT:
                        childLeft = getPaddingLeft() + x - childWidth;
                        childTop = getPaddingTop() + y - childHeight / 2;
                        break;
                    case LayoutConfig.BOTTOM_LEFT:
                        childLeft = getPaddingLeft() + x;
                        childTop = getPaddingTop() + y - childHeight;
                        break;
                    case LayoutConfig.BOTTOM_CENTER:
                        childLeft = getPaddingLeft() + x - childWidth / 2;
                        childTop = getPaddingTop() + y - childHeight;
                        break;
                    case LayoutConfig.BOTTOM_RIGHT:
                        childLeft = getPaddingLeft() + x - childWidth;
                        childTop = getPaddingTop() + y - childHeight;
                        break;
                }
                childLeft += lp.offsetX;
                childTop += lp.offsetY;

                child.arrange(TileSystem.truncateToInt(childLeft), TileSystem.truncateToInt(childTop),
                        TileSystem.truncateToInt(childWidth), TileSystem.truncateToInt(childHeight));
            }
        }

        if (!isLayoutOccurred()) {
            mLayoutOccurred = true;
            for (OnFirstLayoutListener listener : mOnFirstLayoutListeners) {
                listener.onFirstLayout(this, left, top, left + width, top + height);
            }

            mOnFirstLayoutListeners.clear();
        }
        resetProjection();

        return true;
    }

    /**
     * enables you to add a listener for when the map is ready to go.
     *
     * @param listener
     */
    public void addOnFirstLayoutListener(OnFirstLayoutListener listener) {
        // Don't add if we already have a layout
        if (!isLayoutOccurred())
            mOnFirstLayoutListeners.add(listener);
    }

    public void removeOnFirstLayoutListener(OnFirstLayoutListener listener) {
        mOnFirstLayoutListeners.remove(listener);
    }

    public boolean isLayoutOccurred() {
        return mLayoutOccurred;
    }

    /**
     * activities/fragments using osmdroid should call this to release resources, pause gps, sensors, timers, etc
     *
     * @since 6.0.0
     */
    public void onPause() {
        this.getOverlayManager().onPause();
    }

    /**
     * activities/fragments using osmdroid should call this to release resources, pause gps, sensors, timers, etc
     *
     * @since 6.0.0
     */
    public void onResume() {
        this.getOverlayManager().onResume();
    }

    /**
     * destroys the map view, all references to listeners, all overlays, etc
     */
    public void onDetach() {
        this.getOverlayManager().onDetach(this);
        mTileProvider.detach();
        if (mZoomController != null) {
            mZoomController.onDetach();
        }

        //https://github.com/osmdroid/osmdroid/issues/390
        if (mTileRequestCompleteHandler instanceof SimpleInvalidationHandler) {
            ((SimpleInvalidationHandler) mTileRequestCompleteHandler).destroy();
        }
        mTileRequestCompleteHandler = null;
        if (mProjection != null)
            mProjection.detach();
        mProjection = null;
        mRepository.onDetach();
        mListners.clear();
    }

    @Override
    public boolean onKeyEvent(Component component, KeyEvent keyEvent) {
        final int keyCode = keyEvent.getKeyCode();
        if (keyEvent.isKeyDown()) {
            return getOverlayManager().onKeyDown(keyCode, keyEvent, this);
        } else {
            return getOverlayManager().onKeyUp(keyCode, keyEvent, this);
        }
    }

    protected void computeScroll() {
        if (mScroller == null) { //fix for edit mode in the IDE
            return;
        }
        if (!mIsFlinging) {
            return;
        }
        if (!mScroller.isOverScrolled()) {
            return;
        }
        if (mScroller.isFinished()) {
            // we deliberately ignore the very last scrollTo, which sometimes provokes map hiccups
            mIsFlinging = false;
        } else {
            final int axisX = mScroller.getCurrValue(ScrollHelper.AXIS_X);
            final int axisY = mScroller.getCurrValue(ScrollHelper.AXIS_Y);
            scrollTo(axisX, axisY);
//            postInvalidate();
        }
    }

    @Override
    public void scrollTo(int x, int y) {
        setMapScroll(x, y);
        resetProjection();
        invalidate();

        // Force a layout, so that children are correctly positioned according to map orientation
        if (getMapOrientation() != 0f)
            onArrange(getLeft(), getTop(), getRight(), getBottom());

        // do callback on listener
        for (MapListener mapListener : mListners) {
            mapListener.onScroll(new ScrollEvent(this, x, y));
        }
    }

    /**
     * @since 6.0.0
     */
    @Override
    public void scrollBy(int x, int y) {
        scrollTo((int) (getMapScrollX() + x), (int) (getMapScrollY() + y));
    }

    @Override
    public void setBackgroundColor(final int pColor) {
        mMapOverlay.setLoadingBackgroundColor(pColor);
        invalidate();
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        final long startMs = System.currentTimeMillis();

        // Reset the projection
        resetProjection();

        // Apply the scale and rotate operations
        getProjection().save(canvas, true, false);

        /* Draw background */
        // c.drawColor(mBackgroundColor);
        try {
            /* Draw all Overlays. */
            this.getOverlayManager().onDraw(canvas, this);
            // Restore the canvas matrix
            getProjection().restore(canvas, false);
            if (mZoomController != null) {
                mZoomController.draw(canvas);
            }
        } catch (Exception ex) {
            //for edit mode
            Log.e(IMapView.LOGTAG, "error dispatchDraw, probably in edit mode", ex);
        }
        if (Configuration.getInstance().isDebugMapView()) {
            final long endMs = System.currentTimeMillis();
            Log.d(IMapView.LOGTAG, "Rendering overall: " + (endMs - startMs) + "ms");
        }
    }

    @Override
    public void onComponentBoundToWindow(Component component) {

    }

    @Override
    public void onComponentUnboundFromWindow(Component component) {
        if (mDestroyModeOnDetach) {
            onDetach();
        }
    }

    // ===========================================================
    // Animation
    // ===========================================================

    /**
     * Determines if maps are animating a zoom operation. Useful for overlays to avoid recalculating
     * during an animation sequence.
     *
     * @return boolean indicating whether view is animating.
     */
    public boolean isAnimating() {
        return mIsAnimating.get();
    }

    /**
     * @since 6.0.0
     */
    public void resetMultiTouchScale() {
        mMultiTouchScaleCurrentPoint = null;
    }

    /**
     * @since 6.0.0
     */
    protected void setMultiTouchScaleInitPoint(final float pX, final float pY) {
        mMultiTouchScaleInitPoint.modify(pX, pY);
        final Point unRotatedPixel = getProjection().unrotateAndScalePoint((int) pX, (int) pY, null);
        getProjection().fromPixels(unRotatedPixel.getPointXToInt(), unRotatedPixel.getPointYToInt(), mMultiTouchScaleGeoPoint);
        setMultiTouchScaleCurrentPoint(pX, pY);
    }

    /**
     * @since 6.0.0
     */
    protected void setMultiTouchScaleCurrentPoint(final float pX, final float pY) {
        mMultiTouchScaleCurrentPoint = new Point(pX, pY);
    }

    /**
     * @since 6.0.0
     */
    protected void setMultiTouchScale(final float pMultiTouchScale) {
        setZoomLevel(Math.log(pMultiTouchScale) / Math.log(2) + mStartAnimationZoom);
    }

    /**
     * @since 6.0.0
     */
    protected void startAnimation() {
        mStartAnimationZoom = getZoomLevelDouble();
    }

    /*
     * Set the MapListener for this view
     * @deprecated use addMapListener instead
     */
    @Deprecated
    public void setMapListener(final MapListener ml) {
        this.mListners.add(ml);
    }

    /**
     * Just like the old setMapListener, except it supports more than one
     *
     * @param mapListener
     * @since 6.0.0
     */
    public void addMapListener(MapListener mapListener) {
        this.mListners.add(mapListener);
    }

    /**
     * Removes a map listener
     *
     * @param mapListener
     * @since 6.0.0
     */
    public void removeMapListener(MapListener mapListener) {
        this.mListners.remove(mapListener);
    }


    // ===========================================================
    // Methods
    // ===========================================================

    private void checkZoomButtons() {
        this.mZoomController.setZoomInEnabled(canZoomIn());
        this.mZoomController.setZoomOutEnabled(canZoomOut());
    }

    /**
     * @deprecated Use {@link #getZoomController().setVisibility()} instead
     */
    @Deprecated
    public void setBuiltInZoomControls(final boolean on) {
        mZoomController.setVisibility(
                on ? CustomZoomButtonsController.Visibility.SHOW_AND_FADEOUT
                        : CustomZoomButtonsController.Visibility.NEVER);
    }

    public void setMultiTouchControls(final boolean enabled) {
        this.mMultiTouchControlsEnable = enabled;
    }

    public final boolean isMultiTouchControlsEnable(){
        return mMultiTouchControlsEnable;
    }

    /**
     * @return
     * @since 6.0.0
     */
    public boolean isHorizontalMapRepetitionEnabled() {
        return horizontalMapRepetitionEnabled;
    }

    /**
     * If horizontalMapRepetition is enabled the map repeats in left/right direction and scrolling wraps around the
     * edges. If disabled the map is only shown once for the horizontal direction. Default is true.
     *
     * @param horizontalMapRepetitionEnabled
     * @since 6.0.0
     */
    public void setHorizontalMapRepetitionEnabled(boolean horizontalMapRepetitionEnabled) {
        this.horizontalMapRepetitionEnabled = horizontalMapRepetitionEnabled;
        mMapOverlay.setHorizontalWrapEnabled(horizontalMapRepetitionEnabled);
        resetProjection();
        this.invalidate();
    }

    /**
     * @return
     * @since 6.0.0
     */
    public boolean isVerticalMapRepetitionEnabled() {
        return verticalMapRepetitionEnabled;
    }

    /**
     * If verticalMapRepetition is enabled the map repeats in top/bottom direction and scrolling wraps around the
     * edges. If disabled the map is only shown once for the vertical direction. Default is true.
     *
     * @param verticalMapRepetitionEnabled
     * @since 6.0.0
     */
    public void setVerticalMapRepetitionEnabled(boolean verticalMapRepetitionEnabled) {
        this.verticalMapRepetitionEnabled = verticalMapRepetitionEnabled;
        mMapOverlay.setVerticalWrapEnabled(verticalMapRepetitionEnabled);
        resetProjection();
        this.invalidate();
    }

    private ITileSource getTileSourceFromAttributes(final AttrSet aAttributeSet) {
        ITileSource tileSource = TileSourceFactory.DEFAULT_TILE_SOURCE;
        Log.i(IMapView.LOGTAG, "Using tile source: " + tileSource.name());
        return tileSource;
    }


    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================

    private class MapViewScaledListener implements ScaledListener {

        @Override
        public void onScaleStart(Component component, ScaleInfo scaleInfo) {
            if (isMultiTouchControlsEnable()) {
                if (!isAnimating()) {
                    setMultiTouchScaleInitPoint(scaleInfo.startPoint.getPointX(), scaleInfo.startPoint.getPointY());
                    startAnimation();
                }
            }
        }

        @Override
        public void onScaleUpdate(Component component, ScaleInfo scaleInfo) {
            if (isMultiTouchControlsEnable()) {
                Point updatePoint = scaleInfo.updatePoint;
                setMultiTouchScaleCurrentPoint(updatePoint.getPointX(), updatePoint.getPointY());
                setMultiTouchScale((float) scaleInfo.scale);
                postLayout(); // Allows any views fixed to a Location in the MapView to adjust
                invalidate();
            }
        }

        @Override
        public void onScaleEnd(Component component, ScaleInfo scaleInfo) {
            if (isMultiTouchControlsEnable()) {
                if (mZoomRounding) {
                    mZoomLevel = Math.round(mZoomLevel);
                    invalidate();
                }
                resetMultiTouchScale();
            }
        }
    }

    private class MapViewGestureDetector implements TouchEventListener, DraggedListener,
            DoubleClickedListener, ClickedListener, LongClickedListener {

        private Point mDownPoint;
        private TouchEvent mTouchEvent;
        private TouchEvent mFirstDownTouchEvent;

        @Override
        public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
            if (Configuration.getInstance().isDebugMapView()) {
                Log.d(IMapView.LOGTAG, "onTouchEvent(" + touchEvent + ")");
            }

            if (TouchEvent.PRIMARY_POINT_DOWN == touchEvent.getAction()) {
                mFirstDownTouchEvent = touchEvent;
            }

            if (mZoomController.isTouched(touchEvent)) {
                mZoomController.activate();
                return true;
            }

            // Get rotated event for some touch listeners.
            mTouchEvent = rotateTouchEvent(touchEvent);
//            mTouchEvent = touchEvent;

            if (getOverlayManager().onTouchEvent(mTouchEvent, MapView.this)) {
                return true;
            }

            if (Configuration.getInstance().isDebugMapView()) {
                Log.d(IMapView.LOGTAG, "no-one handled onTouchEvent");
            }

            return true;
        }

        private TouchEvent rotateTouchEvent(TouchEvent rotatedEvent) {
            if (getMapOrientation() == 0) {
                return rotatedEvent;
            }

            MmiPoint mmiPoint = rotatedEvent.getPointerPosition(rotatedEvent.getIndex());
            getProjection().unrotateAndScalePoint((int) mmiPoint.getX(), (int) mmiPoint.getY(),
                    mRotateScalePoint);
//            rotatedEvent.setScreenOffset(mRotateScalePoint.getPointX() - mmiPoint.getX(),
//                    mRotateScalePoint.getPointY() - mmiPoint.getY());
            return rotatedEvent;
        }

        @Override
        public void onDragDown(Component component, DragInfo dragInfo) {
            mDownPoint = dragInfo.downPoint;

            if (getOverlayManager().onDown(mTouchEvent, MapView.this)) {
                return;
            }

            if (mZoomController != null) {
                mZoomController.activate();
            }
        }

        @Override
        public void onDragStart(Component component, DragInfo dragInfo) {
            if (!isAnimating()) {
                setMultiTouchScaleInitPoint(dragInfo.startPoint.getPointX(), dragInfo.startPoint.getPointY());
                startAnimation();
            }
        }

        @Override
        public void onDragUpdate(Component component, DragInfo dragInfo) {
            if (getOverlayManager().onScroll(mFirstDownTouchEvent, mTouchEvent, (float) dragInfo.xOffset, (float) dragInfo.yOffset, MapView.this)) {
                return;
            }

            Point updatePoint = dragInfo.updatePoint;
            setMultiTouchScaleCurrentPoint(updatePoint.getPointX(), updatePoint.getPointY());
            postLayout(); // Allows any views fixed to a Location in the MapView to adjust
            invalidate();
        }


        @Override
        public void onDragEnd(Component component, DragInfo dragInfo) {
            if (getOverlayManager().onFling(mFirstDownTouchEvent, mTouchEvent, (float) dragInfo.xVelocity, (float) dragInfo.yVelocity, MapView.this)) {
                return;
            }

            if (mImpossibleFlinging) {
                mImpossibleFlinging = false;
                return;
            }

            mIsFlinging = true;
            if (mScroller != null) {  //fix for edit mode in the IDE
                mScroller.doFling((int) getMapScrollX(), (int) getMapScrollY(), -(int) dragInfo.xVelocity, -(int) dragInfo.yVelocity,
                        Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
            }
        }

        @Override
        public void onDragCancel(Component component, DragInfo dragInfo) {

        }

        @Override
        public void onDoubleClick(Component component) {
            if (getOverlayManager().onDoubleTap(mTouchEvent, MapView.this)) {
                return;
            }

            if (mDownPoint != null) {
                getProjection().rotateAndScalePoint(mDownPoint.getPointXToInt(), mDownPoint.getPointYToInt(), mRotateScalePoint);
                getController().zoomInFixing(mRotateScalePoint.getPointXToInt(), mRotateScalePoint.getPointYToInt());
            }
        }

        @Override
        public void onClick(Component component) {
            getOverlayManager().onSingleTapConfirmed(mTouchEvent, MapView.this);
        }

        @Override
        public void onLongClicked(Component component) {
            MapView.this.getOverlayManager().onLongPress(mTouchEvent, MapView.this);
        }
    }

    private class MapViewZoomListener implements CustomZoomButtonsController.OnZoomListener {
        @Override
        public void onZoom(final boolean zoomIn) {
            if (zoomIn) {
                getController().zoomIn();
            } else {
                getController().zoomOut();
            }
        }

        @Override
        public void onVisibilityChanged(final boolean visible) {
        }
    }

    // ===========================================================
    // Public Classes
    // ===========================================================

    /**
     * Per-child layout information associated with OpenStreetMapView.
     */
    public static class LayoutConfig extends ComponentContainer.LayoutConfig {

        /**
         * Special value for the alignment requested by a View. TOP_LEFT means that the location
         * will at the top left the View.
         */
        public static final int TOP_LEFT = 1;
        /**
         * Special value for the alignment requested by a View. TOP_RIGHT means that the location
         * will be centered at the top of the View.
         */
        public static final int TOP_CENTER = 2;
        /**
         * Special value for the alignment requested by a View. TOP_RIGHT means that the location
         * will at the top right the View.
         */
        public static final int TOP_RIGHT = 3;
        /**
         * Special value for the alignment requested by a View. CENTER_LEFT means that the location
         * will at the center left the View.
         */
        public static final int CENTER_LEFT = 4;
        /**
         * Special value for the alignment requested by a View. CENTER means that the location will
         * be centered at the center of the View.
         */
        public static final int CENTER = 5;
        /**
         * Special value for the alignment requested by a View. CENTER_RIGHT means that the location
         * will at the center right the View.
         */
        public static final int CENTER_RIGHT = 6;
        /**
         * Special value for the alignment requested by a View. BOTTOM_LEFT means that the location
         * will be at the bottom left of the View.
         */
        public static final int BOTTOM_LEFT = 7;
        /**
         * Special value for the alignment requested by a View. BOTTOM_CENTER means that the
         * location will be centered at the bottom of the view.
         */
        public static final int BOTTOM_CENTER = 8;
        /**
         * Special value for the alignment requested by a View. BOTTOM_RIGHT means that the location
         * will be at the bottom right of the View.
         */
        public static final int BOTTOM_RIGHT = 9;
        /**
         * The location of the child within the map view.
         */
        public IGeoPoint geoPoint;

        /**
         * The alignment the alignment of the view compared to the location.
         */
        public int alignment;

        public int offsetX;
        public int offsetY;

        /**
         * Creates a new set of layout parameters with the specified width, height and location.
         *
         * @param width     the width, either {@link #MATCH_PARENT}, {@link #MATCH_CONTENT} or a fixed size
         *                  in pixels
         * @param height    the height, either {@link #MATCH_PARENT}, {@link #MATCH_CONTENT} or a fixed size
         *                  in pixels
         * @param geoPoint  the location of the child within the map view
         * @param alignment the alignment of the view compared to the location {@link #BOTTOM_CENTER},
         *                  {@link #BOTTOM_LEFT}, {@link #BOTTOM_RIGHT} {@link #TOP_CENTER},
         *                  {@link #TOP_LEFT}, {@link #TOP_RIGHT}
         * @param offsetX   the additional X offset from the alignment location to draw the child within
         *                  the map view
         * @param offsetY   the additional Y offset from the alignment location to draw the child within
         *                  the map view
         */
        public LayoutConfig(final int width, final int height, final IGeoPoint geoPoint,
                            final int alignment, final int offsetX, final int offsetY) {
            super(width, height);
            if (geoPoint != null) {
                this.geoPoint = geoPoint;
            } else {
                this.geoPoint = new GeoPoint(0d, 0d);
            }
            this.alignment = alignment;
            this.offsetX = offsetX;
            this.offsetY = offsetY;
        }

        /**
         * Since we cannot use XML files in this project this constructor is useless. Creates a new
         * set of layout parameters. The values are extracted from the supplied attributes set and
         * context.
         *
         * @param c     the application environment
         * @param attrs the set of attributes fom which to extract the layout parameters values
         */
        public LayoutConfig(final Context c, final AttrSet attrs) {
            super(c, attrs);
            this.geoPoint = new GeoPoint(0d, 0d);
            this.alignment = BOTTOM_CENTER;
        }

        public LayoutConfig(final LayoutConfig source) {
            super(source);
        }

    }


    /**
     * enables you to programmatically set the tile provider (zip, assets, sqlite, etc)
     *
     * @param base
     * @see MapTileProviderBasic
     * @since 4.4
     */
    public void setTileProvider(final MapTileProviderBase base) {
        this.mTileProvider.detach();
        mTileProvider.clearTileCache();
        this.mTileProvider = base;
        mTileProvider.getTileRequestCompleteHandlers().add(mTileRequestCompleteHandler);
        updateTileSizeForDensity(mTileProvider.getTileSource());

        this.mMapOverlay = new TilesOverlay(mTileProvider, this.getContext(), horizontalMapRepetitionEnabled, verticalMapRepetitionEnabled);

        mOverlayManager.setTilesOverlay(mMapOverlay);
        invalidate();
    }

    /**
     * Sets the initial center point of the map. This can be set before the map view is 'ready'
     * meaning that it can be set and honored with the onFirstLayoutListener
     *
     * @since 6.0.0
     */
    @Deprecated
    public void setInitCenter(final IGeoPoint geoPoint) {
        setExpectedCenter(geoPoint);
    }

    public long getMapScrollX() {
        return mMapScrollX;
    }

    public long getMapScrollY() {
        return mMapScrollY;
    }

    void setMapScroll(final long pMapScrollX, final long pMapScrollY) {
        mMapScrollX = pMapScrollX;
        mMapScrollY = pMapScrollY;
        postLayout(); // Allows any views fixed to a Location in the MapView to adjust
    }

    /**
     * Should never be used except by the constructor of Projection.
     * Most of the time you'll want to call {@link #getMapCenter()}.
     * <p>
     * This method gives to the Projection the desired map center, typically set by
     * MapView.setExpectedCenter when you want to center a map on a particular point.
     * <a href="https://github.com/osmdroid/osmdroid/issues/868">see issue 868</a>
     *
     * @see #getMapCenter()
     * @since 6.0.0
     */
    GeoPoint getExpectedCenter() {
        return mCenter;
    }

    /**
     * Deferred setting of the expected next map center computed by the Projection's constructor,
     * with no guarantee it will be 100% respected.
     * <a href="https://github.com/osmdroid/osmdroid/issues/868">see issue 868</a>
     *
     * @since 6.0.3
     */
    public void setExpectedCenter(final IGeoPoint pGeoPoint, final long pOffsetX, final long pOffsetY) {
        final GeoPoint before = getProjection().getCurrentCenter();
        mCenter = (GeoPoint) pGeoPoint;
        setMapScroll(-pOffsetX, -pOffsetY);
        resetProjection();
        final GeoPoint after = getProjection().getCurrentCenter();
        if (!after.equals(before)) {
            ScrollEvent event = null;
            for (MapListener mapListener : mListners) {
                mapListener.onScroll(event != null ? event : (event = new ScrollEvent(this, 0, 0)));
            }
        }
        invalidate();
    }

    /**
     * @since 6.0.0
     */
    public void setExpectedCenter(final IGeoPoint pGeoPoint) {
        setExpectedCenter(pGeoPoint, 0, 0);
    }

    /**
     * @since 6.0.2
     */
    public void setZoomRounding(final boolean pZoomRounding) {
        mZoomRounding = pZoomRounding;
    }

    /**
     * @since 6.0.2
     */
    public static TileSystem getTileSystem() {
        return mTileSystem;
    }

    /**
     * @since 6.0.2
     */
    public static void setTileSystem(final TileSystem pTileSystem) {
        mTileSystem = pTileSystem;
    }

    /**
     * @since 6.0.3
     */
    public MapViewRepository getRepository() {
        return mRepository;
    }

    /**
     * @since 6.1.0
     */
    public CustomZoomButtonsController getZoomController() {
        return mZoomController;
    }

    /**
     * @since 6.1.0
     */
    public TilesOverlay getMapOverlay() {
        return mMapOverlay;
    }

    /**
     * @since 6.1.0
     */
    public void setDestroyMode(final boolean pOnDetach) {
        mDestroyModeOnDetach = pOnDetach;
    }

    /**
     * @since 6.1.1
     */
    public int getMapCenterOffsetX() {
        return mMapCenterOffsetX;
    }

    /**
     * @since 6.1.1
     */
    public int getMapCenterOffsetY() {
        return mMapCenterOffsetY;
    }

    /**
     * @since 6.1.1
     */
    public void setMapCenterOffset(final int pMapCenterOffsetX, final int pMapCenterOffsetY) {
        mMapCenterOffsetX = pMapCenterOffsetX;
        mMapCenterOffsetY = pMapCenterOffsetY;
    }

    public final void post(Runnable runnable) {
        new EventHandler(EventRunner.getMainEventRunner()).postTask(runnable);
    }

    public final void postInvalidate() {
        post(this::invalidate);
    }

}

